//*********************************************************
//
//    Copyright (c) Microsoft. All rights reserved.
//    This code is licensed under the Apache License, Version 2.0.
//    THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF
//    ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY
//    IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR
//    PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.
//
//*********************************************************

using System;
using System.Collections.Generic;
using System.Text;

namespace WorkflowData
{
    /// <summary>
    /// For each HyperCube there needs to be a HyperCubeSchema. The
    /// schema object defines how many data columns are to be found
    /// at the data points within the HyperCube. It also keeps track
    /// of a name and description of those points as well as data
    /// type.
    /// 
    /// Column names are forced to be unique. Column data types may be
    /// shared across columns. A validity check is available. A scheme
    /// is valid if every column is named. Keep in mind that trying to
    /// set a column name that all ready exists will throw an exception
    /// and leave that column unnamed (null). Data types default to
    /// the object base type until specified.
    /// 
    /// Note: Column name comparison is case insensitive.
    /// 
    /// Invalid schemas are still usable. However, column lookup by name
    /// will not fully cover the data.
    /// </summary>
    [Serializable]
    public class HyperCubeSchema
    {
        // Linear storage and searching seems sufficient for all common
        // use cases so far encountered. Moving to a dictionary may be
        // necessary if schemas are created with hundreds or more columns
        private string[] colNames;
        private string[] colDesc;
        private Type[] colType;
        // The elements in this array are flags which indicate the dimensions 
        // for the variable at the given index.
        private int[] colDimensions;

        private string xDimName;
        private string yDimName;
        private string zDimName;
        private string tDimName;

        private Type xDimType;
        private Type yDimType;
        private Type zDimType;
        private Type tDimType;

        private const int xDimension = 1;
        private const int yDimension = 2;
        private const int zDimension = 4;
        private const int tDimension = 8;

        // List of dictionaries for storing the variable attribute values
        private Dictionary<string, object>[] attributeColl;

        /// <summary>
        /// Creates a HyperCubeSchema with the given size. Types for the
        /// columns will all be set to the base object type. Names and
        /// descriptions will be null, and thus the schema will be invalid
        /// until column names are set.
        /// </summary>
        /// <param name="columnCount">
        /// The number of columns at each data point in the HyperCube. An
        /// exception will be raised if this number is not a positive integer.
        /// </param>
        public HyperCubeSchema(int columnCount) : this(columnCount, typeof(object)) { }

        /// <summary>
        /// Creates a HyperCubeSchema with the given size. The type of each
        /// column will be set to the same type, specficied in the parameter
        /// to the constructor. Names and descriptions will be null, and thus
        /// the schema will be invalid until column names are set.
        /// </summary>
        /// <param name="columnCount">
        /// The number of columns at each data point in the HyperCube. An
        /// exception will be raised if this number is not a positive integer.
        /// </param>
        /// <param name="commonDataType">
        /// The initial data type assigned to each column.
        /// </param>
        /// <see>HyperCubeSchema()</see>
        public HyperCubeSchema(int columnCount, Type commonDataType)
        {
            if (columnCount <= 0)
                throw new ArgumentException("HyperCubeSchema requires a positive column count");

            this.colNames = new string[columnCount];
            this.colDesc = new string[columnCount];
            this.colType = new Type[columnCount];
            this.colDimensions = new int[columnCount];
            this.attributeColl = new Dictionary<string, object>[columnCount];

            // Reset to the object type if nothing is specified
            if (commonDataType == null)
                commonDataType = typeof(object);

            for (int i = 0; i < columnCount; i++)
            {
                this.colType[i] = commonDataType;
            }
        }

        /// <summary>
        /// The number of columns specified by the schema. This
        /// number may only be set by the constructor.
        /// </summary>
        public int ColumnCount
        {
            get { return colNames.Length; }
        }

        /// <summary>
        /// An indication as to whether or not all column names
        /// have been set and thus whether each column can be
        /// uniquely identified by name.
        /// </summary>
        public bool IsValid
        {
            get
            {
                foreach (string name in colNames)
                {
                    if (name == null) return false;
                }
                return true;
            }
        }

        /// <summary>
        /// The name of the variables stored in the X dimension (e.g. 'Lattitude')
        /// </summary>
        public string XDimensionName
        {
            get { return xDimName; }
            set { xDimName = value; }
        }

        /// <summary>
        /// The name of the variables stored in the Y dimension (e.g. 'Longitude')
        /// </summary>
        public string YDimensionName
        {
            get { return yDimName; }
            set { yDimName = value; }
        }

        /// <summary>
        /// The name of the variables stored in the Z dimension (e.g. 'Height')
        /// </summary>
        public string ZDimensionName
        {
            get { return zDimName; }
            set { zDimName = value; }
        }

        /// <summary>
        /// The name of the variables stored in the T dimension (e.g. 'Timestamp')
        /// </summary>
        public string TDimensionName
        {
            get { return tDimName; }
            set { tDimName = value; }
        }

        /// <summary>
        /// The object type of the values stored for the X dimension
        /// </summary>
        public Type XDimensionType
        {
            get { return xDimType; }
            set { xDimType = value; }
        }

        /// <summary>
        /// The object type of the values stored for the Y dimension
        /// </summary>
        public Type YDimensionType
        {
            get { return yDimType; }
            set { yDimType = value; }
        }

        /// <summary>
        /// The object type of the values stored for the Z dimension
        /// </summary>
        public Type ZDimensionType
        {
            get { return zDimType; }
            set { zDimType = value; }
        }

        /// <summary>
        /// The object type of the values stored for the T dimension
        /// </summary>
        public Type TDimensionType
        {
            get { return tDimType; }
            set { tDimType = value; }
        }

        /// <summary>
        /// Retrieves the column name for the given column index.
        /// Null is returned if the name has not yet been set.
        /// </summary>
        public string GetColumnName(int index)
        {
            CheckIndex(index);
            return colNames[index];
        }

        /// <summary>
        /// Retrieves the column description for the given column index.
        /// Null is returned if the description has not yet been set.
        /// </summary>
        public string GetColumnDescription(int index)
        {
            CheckIndex(index);
            return colDesc[index];
        }

        /// <summary>
        /// Retrieves the type of the data being placed into the column
        /// for the given column index.
        /// </summary>
        public Type GetColumnType(int index)
        {
            CheckIndex(index);
            return colType[index];
        }

        /// <summary>
        /// Looks up the column index based on the column name. The lookup
        /// normalizes whitespace and performs a case insensitive comparison.
        /// </summary>
        /// <param name="colName"></param>
        /// <returns></returns>
        public int LookupColumnIndex(string colName)
        {
            for (int i = 0; i < ColumnCount; i++)
            {
                if (String.IsNullOrEmpty(colNames[i]))
                {
                    continue;
                }
                colName = colName.Normalize();
                if (colNames[i].Equals(colName, StringComparison.InvariantCultureIgnoreCase))
                    return i;
            }
            throw new Exception("Column name '" + colName + "' was not found in the schema");
        }

        /// <summary>
        /// Set the name of the give column index. The name must not be empty
        /// or null. The normalized name must also not exist as a name for any
        /// other column. A case insensitive comparison is used to check for
        /// duplicates.
        /// </summary>
        public void SetName(int index, string name)
        {
            if (String.IsNullOrEmpty(name))
                throw new ArgumentException("Column names can not be empty");
            name = name.Normalize();
            if (String.IsNullOrEmpty(name))
                throw new ArgumentException("Column names can not be empty");

            // Check for duplicate names
            for (int i = 0; i < ColumnCount; i++)
            {
                if (i == index)
                    continue;
                if (name.Equals(colNames[i], StringComparison.InvariantCultureIgnoreCase))
                    throw new ArgumentException("The column name '" + name + "' all ready exists.");
            }

            // Everything passed, set the name
            colNames[index] = name;
        }

        /// <summary>
        /// This method sets the flags to indicate if the variable at the 
        /// index varies across a dimension.
        /// </summary>
        /// <param name="index">Index Val</param>
        /// <param name="axis">Axis Val</param>
        public void SetDimension(int index, HyperCube.Axis axis)
        {
            CheckIndex(index);
            switch (axis)
            {
                case HyperCube.Axis.X:
                    this.colDimensions[index] = this.colDimensions[index] | HyperCubeSchema.xDimension;
                    break;
                case HyperCube.Axis.Y:
                    this.colDimensions[index] = this.colDimensions[index] | HyperCubeSchema.yDimension;
                    break;
                case HyperCube.Axis.Z:
                    this.colDimensions[index] = this.colDimensions[index] | HyperCubeSchema.zDimension;
                    break;
                case HyperCube.Axis.T:
                    this.colDimensions[index] = this.colDimensions[index] | HyperCubeSchema.tDimension;
                    break;
            }
        }

        /// <summary>
        /// This method checks to see if the variable at the index varies
        /// accross the dimnesion specified in the axis.
        /// </summary>
        /// <param name="index">Index Val</param>
        /// <param name="axis">Axis Val</param>
        /// <returns>True if the variable does else false</returns>
        public bool HasDimension(int index, HyperCube.Axis axis)
        {
            CheckIndex(index);
            switch (axis)
            {
                case HyperCube.Axis.X:
                    return (HyperCubeSchema.xDimension == (this.colDimensions[index] & HyperCubeSchema.xDimension) 
                        ? true : false);
                case HyperCube.Axis.Y:
                    return (HyperCubeSchema.yDimension == (this.colDimensions[index] & HyperCubeSchema.yDimension) 
                        ? true : false);
                case HyperCube.Axis.Z:
                    return (HyperCubeSchema.zDimension == (this.colDimensions[index] & HyperCubeSchema.zDimension)
                        ? true : false);
                case HyperCube.Axis.T:
                    return (HyperCubeSchema.tDimension == (this.colDimensions[index] & HyperCubeSchema.tDimension)
                        ? true : false);
            }
            return false;
        }

        /// <summary>
        /// This method determines the dimensions for the the variable at the
        /// specified index and returns the dimension count
        /// </summary>
        public int GetDimensionCount(int colIndex)
        {
            if (colIndex > this.colDimensions.Length)
                throw new ArgumentException("Dimension Index out of Range.");
            
            int dimensionCount = 0;
            dimensionCount = dimensionCount + (this.HasDimension(colIndex, HyperCube.Axis.X) ? 1 : 0);
            dimensionCount = dimensionCount + (this.HasDimension(colIndex, HyperCube.Axis.Y) ? 1 : 0);
            dimensionCount = dimensionCount + (this.HasDimension(colIndex, HyperCube.Axis.Z) ? 1 : 0);
            dimensionCount = dimensionCount + (this.HasDimension(colIndex, HyperCube.Axis.T) ? 1 : 0);

            return dimensionCount;
        }

        /// <summary>
        /// Sets the description for the given column index.
        /// </summary>
        public void SetDescription(int index, string description)
        {
            CheckIndex(index);
            colDesc[index] = description;
        }

        /// <summary>
        /// Sets the data type for the given column index.
        /// </summary>
        public void SetType(int index, Type dataType)
        {
            CheckIndex(index);
            if (dataType == null)
                dataType = typeof(object);
            colType[index] = dataType;
        }

        /// <summary>
        /// This method creates a new HyperCubeSchema object which is
        /// a copy of the this object.
        /// </summary>
        /// <returns></returns>
        public HyperCubeSchema Clone()
        {
            HyperCubeSchema newSchema = new HyperCubeSchema(this.ColumnCount);
            for (int i = 0; i < this.ColumnCount; ++i)
            {
                newSchema.SetName(i, this.GetColumnName(i));
                newSchema.SetType(i, this.GetColumnType(i));
                newSchema.attributeColl[i] = this.attributeColl[i];

                if (this.HasDimension(i, HyperCube.Axis.X))
                    newSchema.SetDimension(i, HyperCube.Axis.X);
                if (this.HasDimension(i, HyperCube.Axis.Y))
                    newSchema.SetDimension(i, HyperCube.Axis.Y);
                if (this.HasDimension(i, HyperCube.Axis.Z))
                    newSchema.SetDimension(i, HyperCube.Axis.Z);
                if (this.HasDimension(i, HyperCube.Axis.T))
                    newSchema.SetDimension(i, HyperCube.Axis.T);
            }

            // Copy the dimension names and types.
            newSchema.XDimensionName = this.XDimensionName;
            newSchema.XDimensionType = this.XDimensionType;

            newSchema.YDimensionName = this.YDimensionName;
            newSchema.YDimensionType = this.YDimensionType;

            newSchema.ZDimensionName = this.ZDimensionName;
            newSchema.ZDimensionType = this.ZDimensionType;

            newSchema.TDimensionName = this.TDimensionName;
            newSchema.TDimensionType = this.TDimensionType;

            return newSchema;
        }

        /// <summary>
        /// This method adds the attribute name and value to the attrib map
        /// for each variable. 
        /// </summary>
        /// <param name="colIndex">Index of the associated variable</param>
        /// <param name="attribName">Name of the attribute</param>
        /// <param name="attribVal">Value for the attribute</param>
        public void AddAttribute(int colIndex, string attribName, object attribVal)
        {
            CheckIndex(colIndex);

            Dictionary<string, object> attribMap = this.attributeColl[colIndex];
            if (null == attribMap)
            {
                attribMap = new Dictionary<string, object>();
                this.attributeColl[colIndex] = attribMap;
            }
            attribMap.Add(attribName, attribVal);
        }

        /// <summary>
        /// This method returns the attribute value for the attribute name passed.
        /// If the attribute name is not found this method returns a null.
        /// </summary>
        /// <param name="colIndex">Index of the associated variable</param>
        /// <param name="attribName">Name of the attribute</param>
        /// <returns></returns>
        public object GetAttribute(int colIndex, string attribName)
        {
            CheckIndex(colIndex);

            Dictionary<string, object> attribMap = this.attributeColl[colIndex];
            if (null == attribMap)
            {
                return null;
            }
            
            object val;
            attribMap.TryGetValue(attribName, out val);
            
            return val;
        }

        /// <summary>
        /// This method returns the attribute map for the variable at the 
        /// index passed.
        /// </summary>
        /// <returns></returns>
        public Dictionary<string, object> GetAllAttributes(int colIndex)
        {
            CheckIndex(colIndex);

            return this.attributeColl[colIndex];
        }

        public override string ToString()
        {
            StringBuilder buff = new StringBuilder();

            buff.AppendFormat("Dimension Names: X '{0}', Y '{1}', Z '{2}', T '{3}'\n", XDimensionName, YDimensionName, ZDimensionName, TDimensionName);
            buff.AppendFormat("Dimension Types: X <{0}>, Y <{1}>, Z <{2}>, T <{3}'\n", XDimensionType, YDimensionType, ZDimensionType, TDimensionType);
            buff.AppendFormat("Columns ({0}): ", this.ColumnCount);
            for (int i=0; i < this.ColumnCount; i++)
            {
                if (i > 0)
                    buff.Append(", ");
                buff.AppendFormat("'{0}'", GetColumnName(i));
            }
            buff.Append("\nColumn Types: ");
            for (int i = 0; i < this.ColumnCount; i++)
            {
                if (i > 0)
                    buff.Append(", ");
                buff.AppendFormat("<{0}>", GetColumnType(i));
            }
            buff.Append("\n");

            return buff.ToString();
        }

        // Helper to check if index is within bounds of the schema
        private void CheckIndex(int index)
        {
            if (index < 0 || index >= ColumnCount)
                throw new IndexOutOfRangeException("Column index out of schema bounds");
        }
    }
}
